package projects;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;

/**
 * Perceptron
 * DeltaW[i,j] = c(t[j] - z[j]) * x[i]
 */
public class Perceptron extends SupervisedLearner {

	private double[][] weights;
	private int numClassesFromLabel;
	private final double LEARN_RATE = 0.001;
	private final double BIAS = 1.0;
	private BufferedWriter writer;
	private int lastNumCorrect;
	private int numCorrect;
	private final String FILE_NAME = "out.csv";

	public Perceptron(Random rand) {
		numCorrect = 0;
		lastNumCorrect = -1;
		try {
			writer = new BufferedWriter(new FileWriter("output/" + FILE_NAME));
		}
		catch(IOException e) {
			e.printStackTrace();
		}
	}	
	
	public void train(Matrix features, Matrix labels) throws Exception {		
		
		// Compute the number of classes are possible for the output label
		int i = 0;
		while(labels.attrValue(0, i++) != null)		
			numClassesFromLabel = i;		
		
		graphPoints(features);		

		// Note: Java inits all the weights to 0.0
		weights = new double[numClassesFromLabel][features.cols() + 1]; 
		int numEpochs = 0;
		
		// Train enough epochs until the number correctly classified doesn't increase
		for (int perceptronNum = 0; perceptronNum < numClassesFromLabel; perceptronNum++) {
			numCorrect = 0; 
			lastNumCorrect = -1;
			while (numCorrectIsIncreasing()) {
				lastNumCorrect = numCorrect;
				weights[perceptronNum] = trainEpoch(features, labels, perceptronNum);
				numEpochs++;
			}
		}
		
		graphSeparationLinePoints();
		writer.close();

		System.out.println("numEpochs: " + numEpochs);
		printWeights();
	}

	public void predict(int row, final double[] features, double[] labels) throws Exception {
			
		for (int i = 0; i < labels.length; i++) {
			double output = 0;
			double prevNet = 0;
			for (int perceptronNum = 0; perceptronNum < numClassesFromLabel; perceptronNum++) {
				double net = 0.0;
				for (int j = 0; j < features.length; j++)
					net += weights[perceptronNum][j] * features[j];
				net += weights[perceptronNum][weights.length - 1] * BIAS;
				
				if (net > 0 && net > prevNet) {
					output = perceptronNum;
					prevNet = net;
				}
			}
			labels[i] = output;
		}
	}	
	
	private double[] trainEpoch(Matrix features, Matrix labels, int perceptronNum) {
		 
		numCorrect = 0;
				
		double[] weights = new double[features.cols() + 1];
		double[] changeInWeights = new double[weights.length];
		
		for (int row = 0; row < features.rows(); row++) {	
		
			// store the target value
			double target = labels.get(row, 0);	
			if (target == perceptronNum)
				target = 1;
			else target = 0;
			
			// net = (features [dot-product] weights)
			double net = 0.0;			
			for (int col = 0; col < features.cols(); col++) 
				net += features.get(row, col) * weights[col];				
			net += BIAS * weights[weights.length - 1];	
			
			// compute binary output based on net value
			double output = 0;
			if (net > 0) output = 1;			
			
			if (Math.abs(target - output) < 0.01)
				numCorrect++;

			// compute changeInWeights vector
			for (int i = 0; i < weights.length - 1; i++) 
				changeInWeights[i] = LEARN_RATE * (target - output) * features.get(row, i);
			changeInWeights[changeInWeights.length - 1] = LEARN_RATE * (target - output) * BIAS;

			// add each changeInWeight to weight
			for (int i = 0; i < weights.length; i++) 
				weights[i] += changeInWeights[i];
		}		
			
		return weights;
	}	
	
	private boolean numCorrectIsIncreasing() {
		return numCorrect > lastNumCorrect;
	}
	
	private void graphPoints(Matrix features) {
		try {
			writer.write("\nFeature Points\nx, y\n");
			for (int row = 0; row < features.rows(); row++) {
				for (int col = 0; col < features.cols(); col++) {
					writer.write(String.format("%.2f", features.get(row, col)));
					if (col < features.cols() - 1)
						writer.write(", ");
				}
				writer.write("\n");
			}
		}
		catch(IOException e) {
			e.printStackTrace();
		}
	}
	
	private void graphSeparationLinePoints() {
		try {
			double c1 = weights[0][0];
			double c2 = weights[0][1];
			double c3 = weights[0][2];
			
			double slope = -c1 / c2;
			double intercept = c3 / c2;		
			
			// print out a point on the left
			double y = slope * -1 + intercept;		
			writer.write("\nLine Points\nx,y\n-1," + y + "\n");
			
			// print out a point on the right
			y = slope * 1 + intercept;		
			writer.write("1," + y + "\n");
		}
		catch(IOException e) {
			e.printStackTrace();
		}
	}
	
	public void printWeights() {
		System.out.print("\nWeights: [\t");
		for (double weight : weights[2])
			System.out.print(weight + "\t");
		System.out.println("]\n");
	}

}
